package tetris.game.model;

import java.util.List;
import java.util.Random;
import java.util.Vector;

import android.graphics.Point;
import android.util.Log;

/**
 * Represents the Tetris game's field (or grid).
 *
 */
public class GameField {

	public static final int FIELD_WIDTH = 10;
	
	public static final int FIELD_HEIGHT = 20;
	
	/**
	 * The height of the current static fields (for mp only)
	 */
	private int height = 0;
	
	private Random random = new Random(System.currentTimeMillis());
	
	/**
	 * First dimension is y-axis (height) and second is x-axis (width)
	 */
	private Field[] [] field = new Field [FIELD_HEIGHT] [FIELD_WIDTH];
	
	public GameField() {
		// initialize new GameField with all fields in black ( = empty)
		for(int i = 0; i < field.length; i++) {
			for(int j = 0; j < field[i].length; j++)
				field[i] [j] = new Field(Color.BLACK, false);
		}
	}
	
	/**
	 * Call this method every time after doGravity() to try to eliminate full rows
	 * @return the number of rows that have been eliminated
	 */
	int eliminateRows() {
		int rows = 0;
		int rowNumber;
		boolean rowIsFull;
		/*
		 * Perform row elimination in a bubble sort style. The highest row number that is full 
		 * (= the bottom closest row) will be eliminated.
		 * Not most effective, but easy to implement.
		 */
		do {
			rowNumber = -1;		// - 1 indicates that no candidate for elimination could be found
			// go over all rows
			for(int i = 0; i < FIELD_HEIGHT; i++) {
				rowIsFull = true;
				// check every column for this row if there are no gaps
				for(int j = 0; j < FIELD_WIDTH; j++) {
					if(field[i] [j].getColor().equals(Color.BLACK))
						rowIsFull = false;
				}
				if(rowIsFull)
					rowNumber = i;
			}
			// do the actual elimination 
			if(rowNumber != -1) {
				rows++;
				for(int i = rowNumber; i > 0; i--) {
					for(int j = 0; j < FIELD_WIDTH; j++) {
						// set the field above on the lower position
						field[i] [j].setColor(field[i-1] [j].getColor());
						field[i] [j].setDynamic(field[i-1] [j].isDynamic());
					}
				}
			}
		} while(rowNumber != -1);
		return rows;
	}
	
	/**
	 * Tries to move the dynamic fields of the block one position down if possible.
	 * @return true, if a new Block is required for the game, 
	 * 			because the current one got stuck and is now static
	 */
	boolean doGravity() {
		// first check if the block can sink
		boolean canSink = true;
		for(int i = FIELD_HEIGHT-1; i >= 0; i--) {
			for(int j = 0; j < field[i].length; j++) {
				// check for each found dynamic block
				if (field[i] [j].isDynamic()) {
					// if the current row is the bottom or the field below is already used, the block can't sink any further
					// FIXME: if issues with gravity occur, the second part of the condition could be the cause (not tested yet)
					if (i == FIELD_HEIGHT-1  || 
							(!field[i+1] [j].getColor().equals(Color.BLACK) && field[i+1] [j].isDynamic() == false)) 
						canSink = false;
				}
			}
		}
		// if the block can sink move its fields each one down
		if(canSink) {
			for(int i = FIELD_HEIGHT-1; i >= 0; i--) {
				for(int j = 0; j < field[i].length; j++) {
					// check for each found dynamic block
					if (field[i] [j].isDynamic()) {
						// set new field position with correct attributes
						field[i+1] [j].setColor(field[i] [j].getColor());
						field[i+1] [j].setDynamic(true);
						// reset field of current position
						field[i] [j].setColor(Color.BLACK);
						field[i] [j].setDynamic(false);
					}
				}
			}
		}
		// else make all of its fields static
		else {
			for(int i = FIELD_HEIGHT-1; i >= 0; i--) {
				for(int j = 0; j < field[i].length; j++) {
					// check for each found dynamic block
					if (field[i] [j].isDynamic()) {
						field[i] [j].setDynamic(false);
					}
				}
			}
		}
		
		// update the current height value (determine the highest static field in the grid, 
		// which does not have the background color)
		for(int i = 0; i < FIELD_HEIGHT; i++) {
			for(int j = 0; j < FIELD_WIDTH; j++) {
				if(!field[i] [j].isDynamic() && !field[i] [j].getColor().equals(Color.BLACK)) {
					this.height = FIELD_HEIGHT - i;
					break;
				}
			}
		}
		
		return !canSink;
	}
	
	/**
	 * Moves the dynamic block in the desired direction if possible, else does nothing.
	 * @param moveRight - true for direction right or false for direction left
	 */
	void doMove(boolean moveRight) {
		// first check if this move is possible
		// this means no field of the block may be in the most left column (for move left)
		// and no field in the most right column (for move right), otherwise the move would step over the border
		if(moveRight) {
			for(int i = 0; i < FIELD_HEIGHT; i++) {
				if(field[i] [FIELD_WIDTH-1].isDynamic())
					return;
			}
		}
		else {
			for(int i = 0; i < FIELD_HEIGHT; i++) {
				if(field[i] [0].isDynamic())
					return;
			}
		}
		
		// use this variable to derive the direction of the move
		int move = -1;
		if (moveRight)
			move = 1;
		boolean movePossible = true;
		// save all the points that should be moved in List, otherwise some of them get lost
		List<Point> moveFrom = new Vector<Point> ();
		
		// check if the move is possible (no collision with static occupied fields)
		for(int i = 0; i < FIELD_HEIGHT; i++) {
			// this loop won't step over the borders, because the check at the beginning of this method avoids it
			for(int j = 0; j < FIELD_WIDTH; j++) {
				// check for each found dynamic block
				if (field[i] [j].isDynamic()) {
					moveFrom.add(new Point(i, j));
					// if the move would collide with a static block --> not possible
					// the neighbor field there this block should be moved therefore must have the Color.BLACK
					// OR be also a dynamic field which will also be moved and so it frees it space for this one
					if ( ( !field[i] [j+move].getColor().equals(Color.BLACK) && !field[i] [j+move].isDynamic() ) ) 
						movePossible = false;
				}
			}
		}
		
		// perform the desired action if no problems were detected
		if(movePossible && !moveFrom.isEmpty()) {
			// first retrieve the color, which could otherwise be overwritten later
			Point first = moveFrom.get(0);
			// save color of the current block, because it could be overwritten for some fields later
			Color bColor = field[first.x] [first.y].getColor();
			// now reset all current positions
			for(Point p : moveFrom) {
				// reset the old position
				field[p.x] [p.y].setColor(Color.BLACK);
				field[p.x] [p.y].setDynamic(false);
			}
			// finally set the new positions (this approach avoids that some new occupied fields get overwritten as empty fields)
			for(Point p : moveFrom) {
				// set the new field properly
				field[p.x] [p.y+move].setColor(bColor);
				field[p.x] [p.y+move].setDynamic(true);
			}
		}
	}
	
	/**
	 * Moves the current block down faster. (3 steps at once if possible)
	 */
	void doMoveDown() {
		// get a List of all fields' positions occupied  by the currently active block
		List<Point> moveFrom = new Vector<Point> ();
		for(int i = 0; i < FIELD_HEIGHT; i++) {
			for(int j = 0; j < FIELD_WIDTH; j++) {
				if (field[i] [j].isDynamic())
					moveFrom.add(new Point(i, j));
			}
		}
		
		// now determine the maximum of steps that this block can fall down. (the maximum is predefined as 3)
		// the goal is to leave a gap of one field to a collision, because of problems with row elimination 
		// and new block generation that should be left to doGravity()-method
		
		int max = 3;			// the result can't be higher than this value
		int downStepsPossible;  // number of steps the current block can fall
		boolean collides;		// used to detect if this column would collide with another block 
		for(Point p : moveFrom) {
			downStepsPossible = 0;
			collides = false;
			
			for(int i = p.x; i < FIELD_HEIGHT-1; i++) {
				// no further down move possible if the field is static and does not have the background color (black)
				if(!field[i] [p.y].getColor().equals(Color.BLACK) && !field[i] [p.y].isDynamic()) {
					collides = true;
					break;
				}
				// increase counter on no collision
				downStepsPossible++;
			}
			// ensure that a gap of one row is left to a collision, to leave new block and row elimination to doGravity()
			if(collides)
				downStepsPossible--;
			// check if the maximum must be set down because of found obstacles
			if(downStepsPossible <  max)
				max = downStepsPossible;
		}
		
		// do the actual faster move down only if possible
		if(max > 0 && !moveFrom.isEmpty()) {
			// save the color because it could be overwritten
			Color bColor = field[moveFrom.get(0).x] [moveFrom.get(0).y].getColor();
			// release the previously used fields
			for(Point p : moveFrom) {
				field[p.x] [p.y].setColor(Color.BLACK);
				field[p.x] [p.y].setDynamic(false);
			}
			// fill the new positions
			for(Point p : moveFrom) {
				field[p.x+max] [p.y].setColor(bColor);
				field[p.x+max] [p.y].setDynamic(true);
			}
		}
	}
	
	/**
	 * Insert a number of rows at the bottom (Multiplayer only)
	 * @param rows the number of rows to be inserted at the bottom
	 * @return true on success (or invalid arguments; should never happen), false indicates game over
	 */
	boolean insertRows(int rows) {	
		if(rows < 1 || rows > 4)
			return true;
		int gapPosition = random.nextInt(FIELD_WIDTH);
		// check if there is enough space for the insert of rows
		boolean enoughSpace = true;
		for(int i = 0; i < rows; i++) {
			for(int j = 0; j < FIELD_WIDTH; j++) {
				if(!field[i] [j].isDynamic()) {
					enoughSpace = false;
					break;
				}
			}
		}
		if(!enoughSpace) {
			// game over
			return false;
		}
		// first move all existing fields one position up
		else {
			for(int i = rows; i < FIELD_HEIGHT; i++) {
				for(int j = 0; j < FIELD_WIDTH; j++) {
					field[i-rows] [j].setColor(field[i] [j].getColor());
					field[i-rows] [j].setDynamic(field[i] [j].isDynamic());
				}
			}
			// now insert the new lines to the free positions at the bottom
			for(int i = FIELD_HEIGHT-rows; i < FIELD_HEIGHT; i++) {
				for(int j = 0; j < FIELD_WIDTH; j++) {
					field[i] [j].setDynamic(false);
					// leave a gap in each row
					if(j == gapPosition)
						field[i] [j].setColor(Color.BLACK);
					else 
						field[i] [j].setColor(Color.RED);
				}
			}
		}
		return true;
	}
	
	/**
	 * Performs the rotation of the currently active Block.
	 * @param block the active Block that should be rotated
	 */
	void rotateBlock(Block block) {
		
		// first determine the lowest and central field of the current block
		// this field will be used as base (and lowest position) for the rotated block
		
		// start with getting the lowest point from the left of this block
		List<Point> currentPositions = new Vector<Point> ();
		Point lowestPoint = new Point();
		boolean firstMatchFound = false;
		for(int i = FIELD_HEIGHT-1; i >= 0; i--) {
			for(int j = 0; j < field[i].length; j++) {
				// check for each found dynamic block
				if (field[i] [j].isDynamic()) {
					// save all old fields, to free them later on
					currentPositions.add(new Point(i, j));
					// save only the lowest leftmost field here
					if(!firstMatchFound) {
						lowestPoint.set(i, j);
						firstMatchFound = true;
					}
				}
			}
		}
		// determine the central field of the block 
		int rightBound = lowestPoint.y;
		for(int i = lowestPoint.y+1; i < FIELD_WIDTH; i++) {
			if(field[lowestPoint.x] [i].isDynamic())
				rightBound = i;
		}
		// align column position of "lowestPoint" if necessary
		if(rightBound > lowestPoint.y) {
			rightBound = rightBound + ((lowestPoint.y - rightBound) / 2);
			lowestPoint.set(lowestPoint.x, rightBound);
		}
		
		/*
		 * Different treatment for the single BlockTypes and rotation states is necessary
		 * Block pictures before the single cases show the current state of the block BEFORE the rotation
		 */
		List<Point> newPositions = new Vector<Point> ();
		int x = lowestPoint.x;
		int y = lowestPoint.y;
		switch(block.getBlockType()) {
		// I-Block tested and works
		case I:
			// determine the potential new fields and check the bounds
			// case 1: block orthogonal
			/* X
			 * X
			 * X
			 * X
			 */
			if(block.getRotation() % 180 == 0) {
				newPositions.add(new Point(x, y-1));
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x, y+1));
				newPositions.add(new Point(x, y+2));
			}
			// case 2: block horizontal
			/*
			 * XXXX
			 */
			else {
				newPositions.add(new Point(x, y-1));
				newPositions.add(new Point(x-1, y-1));
				newPositions.add(new Point(x-2, y-1));
				newPositions.add(new Point(x-3, y-1));
			}
			// try to do the rotation
			executeRotation(currentPositions, newPositions, block);
			break;
		// J-Block tested and works
		case J:
			/*  X
			 *  X
			 * XX 
			 */
			if(block.getRotation() == 0) {
				newPositions.add(new Point(x-1, y-1));
				newPositions.add(new Point(x, y-1));
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x, y+1));
			}
			/* X
			 * XXX
			 */
			else if(block.getRotation() == 90) {
				newPositions.add(new Point(x, y-1));
				newPositions.add(new Point(x-1, y-1));
				newPositions.add(new Point(x-2, y-1));
				newPositions.add(new Point(x-2, y));
			}
			/* XX
			 * X
			 * X
			 */
			else if(block.getRotation() == 180) {
				newPositions.add(new Point(x-1, y));
				newPositions.add(new Point(x-1, y-1));
				newPositions.add(new Point(x-1, y+1));
				newPositions.add(new Point(x, y+1));
			}
			/* XXX
			 *   X
			 */
			else {
				newPositions.add(new Point(x, y-1));
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x-1, y));
				newPositions.add(new Point(x-2, y));
			}
			executeRotation(currentPositions, newPositions, block);
			break;
		case L:
			/*  X
			 *  X
			 *  XX
			 */ 
			if(block.getRotation() == 0) {
				newPositions.add(new Point(x, y-1));
				newPositions.add(new Point(x-1, y-1));
				newPositions.add(new Point(x-1, y));
				newPositions.add(new Point(x-1, y+1));
			}
			/* XXX
			 * X
			 */
			else if(block.getRotation() == 90) {
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x-1, y));
				newPositions.add(new Point(x-2, y));
				newPositions.add(new Point(x-2, y-1));
			}
			/* XX
			 *  X
			 *  X
			 */
			else if(block.getRotation() == 180) {
				newPositions.add(new Point(x, y-1));
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x, y+1));
				newPositions.add(new Point(x-1, y+1));
			}
			/*   X
			 * XXX
			 */
			else {
				newPositions.add(new Point(x-2, y));
				newPositions.add(new Point(x-1, y));
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x, y+1));
			}
			executeRotation(currentPositions, newPositions, block);
			break;
		// S-Block tested and works
		case S:
			/*  XX
			 * XX
			 */
			if(block.getRotation() % 180 == 0) {
				newPositions.add(new Point(x+1, y+1));
				newPositions.add(new Point(x-1, y));
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x, y+1));
			}
			/* X
			 * XX
			 *  X
			 */
			else {
				newPositions.add(new Point(x, y-2));
				newPositions.add(new Point(x, y-1));
				newPositions.add(new Point(x-1, y-1));
				newPositions.add(new Point(x-1, y));
			}
			executeRotation(currentPositions, newPositions, block);
			break;
		// Z-Block tested and works
		case Z:
			/* XX
			 *  XX
			 */
			if(block.getRotation() % 180 == 0) {
				newPositions.add(new Point(x+1, y-1));
				newPositions.add(new Point(x, y-1));
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x-1, y));
			}
			/*  X
			 * XX
			 * X
			 */
			else {
				newPositions.add(new Point(x, y+1));
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x-1, y-1));
				newPositions.add(new Point(x-1, y));
			}
			executeRotation(currentPositions, newPositions, block);
			break;
		// T-Block tested and works
		case T:
			/* XXX
			 *  X
			 */
			if(block.getRotation() == 0) {
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x-1, y));
				newPositions.add(new Point(x-1, y-1));
				newPositions.add(new Point(x-2, y));
			}
			/*  X
			 * XX
			 *  X
			 */
			else if(block.getRotation() == 90) {
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x, y-1));
				newPositions.add(new Point(x, y+1));
				newPositions.add(new Point(x-1, y));
			}
			/*  X
			 * XXX
			 */
			else if(block.getRotation() == 180) {
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x-1, y));
				newPositions.add(new Point(x-1, y+1));
				newPositions.add(new Point(x-2, y));
			}
			/* X
			 * XX
			 * X
			 */
			else {
				newPositions.add(new Point(x, y));
				newPositions.add(new Point(x-1, y));
				newPositions.add(new Point(x-1, y+1));
				newPositions.add(new Point(x-1, y-1));
			}
			executeRotation(currentPositions, newPositions, block);
			break;
		// no action required for BlockType "O"
		default:
			System.err.println("rotateBlock() default case reached");
		}
	}
	
	/**
	 * To avoid code redundancy, perform the actual changes on the field array here.
	 * @param from List with the 4 Points to be freed
	 * @param to List with 4 Points to be used instead
	 * @param block the Block to be rotated
	 */
	private void executeRotation(List<Point> from, List<Point> to, Block block) {
		// check if the desired destinations fields are free and inside the GameField
		for(Point p : to) {
//			System.out.println("Row: " + p.x + "   Column:" + p.y);
			// check that no new positions is outside of the borders
			if(p.x < 0 || p.x >= FIELD_HEIGHT || p.y < 0 || p.y >= FIELD_WIDTH)
				return;
			if(!field[p.x] [p.y].getColor().equals(Color.BLACK) && !field[p.x] [p.y].isDynamic())
				return;
		}
		// free the previously used positions
		for(Point p : from) {
			field[p.x] [p.y].setColor(Color.BLACK);
			field[p.x] [p.y].setDynamic(false);
		}
		// occupy the new positions
		for(Point p : to) {
			field[p.x] [p.y].setColor(block.getBlockColor());
			field[p.x] [p.y].setDynamic(true);
		}
		// recalculate the rotation value of this Block
		block.setRotation((block.getRotation() + 90) % 360);
	}
	
	/**
	 * Insert a new block on the top of the GameField.
	 * @param block
	 * @return true if the Block could be inserted OR false if the block can't be inserted --> GameOver
	 */
	boolean addNewBlock(Block block) {
		int x = FIELD_WIDTH / 2; 
		List<Point> positions = new Vector<Point> ();
		/*
		 * For every BlockType determine the positions where the Block should be inserted.
		 */
		switch(block.getBlockType()) {
		/* X
		 * X
		 * X
		 * X
		 */
		case I: 
			if(block.getRotation() % 180 == 0) {
				for(int i = 0; i < 4; i++) 
					positions.add(new Point(i, x));
				break;
			}
			else {
				for(int i = (x-1); i < (x+3); i++) 
					positions.add(new Point(0, i));
				break;
			}
		case J:
			if(block.getRotation()  == 0) {
				positions.add(new Point(0, x));
				positions.add(new Point(1, x));
				positions.add(new Point(2, x));
				positions.add(new Point(2, x-1));
				break;
			}
			else if(block.getRotation() == 90) {
				positions.add(new Point(0, x-1));
				positions.add(new Point(1, x-1));
				positions.add(new Point(1, x));
				positions.add(new Point(1, x+1));
				break;
			}
			else if(block.getRotation() == 180) {
				positions.add(new Point(0, x));
				positions.add(new Point(0, x+1));
				positions.add(new Point(1, x));
				positions.add(new Point(2, x));
				break;
			}
			else {
				positions.add(new Point(0, x-1));
				positions.add(new Point(0, x));
				positions.add(new Point(0, x+1));
				positions.add(new Point(1, x+1));
				break;
			}
		case L:
			if(block.getRotation()  == 0) {
				positions.add(new Point(0, x));
				positions.add(new Point(1, x));
				positions.add(new Point(2, x));
				positions.add(new Point(2, x+1));
				break;
			}
			else if(block.getRotation() == 90) {
				positions.add(new Point(0, x-1));
				positions.add(new Point(1, x-1));
				positions.add(new Point(0, x));
				positions.add(new Point(0, x+1));
				break;
			}
			else if(block.getRotation() == 180) {
				positions.add(new Point(0, x-1));
				positions.add(new Point(0, x));
				positions.add(new Point(1, x));
				positions.add(new Point(2, x));
				break;
			}
			else {
				positions.add(new Point(1, x-1));
				positions.add(new Point(1, x));
				positions.add(new Point(1, x+1));
				positions.add(new Point(0, x+1));
				break;
			}
		case O:
			positions.add(new Point(0, x-1));
			positions.add(new Point(0, x));
			positions.add(new Point(1, x-1));
			positions.add(new Point(1, x));
			break;
		/* 
		 *  XX
		 * XX 
		 */
		case S:
			if(block.getRotation() % 180 == 0) {
				positions.add(new Point(0, x));
				positions.add(new Point(0, x+1));
				positions.add(new Point(1, x));
				positions.add(new Point(1, x-1));
				break;
			}
			else {
				positions.add(new Point(0, x-1));
				positions.add(new Point(1, x-1));
				positions.add(new Point(1, x));
				positions.add(new Point(2, x));
				break;
			}
		/* XXX
		 *  X
		 */
		case T:
			if(block.getRotation() == 0) {
				positions.add(new Point(0, x-1));
				positions.add(new Point(0, x));
				positions.add(new Point(0, x+1));
				positions.add(new Point(1, x));
				break;
			}
			else if(block.getRotation() == 90) {
				positions.add(new Point(0, x));
				positions.add(new Point(1, x));
				positions.add(new Point(1, x-1));
				positions.add(new Point(2, x));
				break;
			}
			else if(block.getRotation() == 180) {
				positions.add(new Point(0, x));
				positions.add(new Point(1, x-1));
				positions.add(new Point(1, x));
				positions.add(new Point(1, x+1));
				break;
			}
			else {
				positions.add(new Point(0, x));
				positions.add(new Point(1, x));
				positions.add(new Point(1, x+1));
				positions.add(new Point(2, x));
				break;
			}
		/*
		 * XX
		 *  XX
		 */
		case Z:
			if(block.getRotation() % 180 == 0) {
				positions.add(new Point(0, x-1));
				positions.add(new Point(0, x));
				positions.add(new Point(1, x));
				positions.add(new Point(1, x+1));
				break;
			}
			else {
				positions.add(new Point(0, x));
				positions.add(new Point(1, x));
				positions.add(new Point(1, x-1));
				positions.add(new Point(2, x-1));
				break;
			}
		// this should never be reached
		default:
			return true;
		}
		// finally try to do the actual insert
		return !insertBlock(block, positions);
	}
	
	/**
	 * Performs the actual insert of a new Block.
	 * @param block the Block to be inserted
	 * @param positions the positions where the Block should be inserted
	 * @return true if the insert was successful, otherwise false
	 */
	private boolean insertBlock(Block block, List<Point> positions) {
		Log.d("GameField", "Try to add block " + block.getBlockType() + " " + 
				block.getBlockColor() + " " + block.getRotation());
		// check if all positions are free
		for(Point p : positions) {
			if(!field[p.x] [p.y].getColor().equals(Color.BLACK))
				return false;
		}
		// do the insert
		for(Point p : positions) {
			field[p.x] [p.y].setColor(block.getBlockColor());
			field[p.x] [p.y].setDynamic(true);
		}
		return true;
	}
	
	/**
	 * Used to enable display of the game's state in UI.
	 * @return
	 */
	public Field[] [] getField() {
		return this.field;
	}

	public int getHeight() {
		return height;
	}
	
	
	
}
